using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using Fuzzware.Common.Encoding;
using Fuzzware.Common.XML;
using Fuzzware.Common.DataSchema;
using Fuzzware.Common.DataSchema.Restrictions;
using Fuzzware.ConvertFromXML.Processors;

namespace Fuzzware.ConvertFromXML
{
    /// <summary>
    /// Contains the value of an XML node.  For nodes that do not have simple content, then the node can contain children that are both
    /// text (actually OutputTextNode's) and other nodes (OutputNode's).  The is also true for nodes that have simple content, as although 
    /// they will most often just contain a string for instance, they can also contain comments, or be CDATA, and so its best to view all
    /// simple content as a child (of type OutputTextNode) of an OutputNode.
    /// 
    /// <ON>
    ///     <ON>
    ///         <OTN>NodeValue</OTN>
    ///     </ON>
    ///     <ON>
    ///         <ON>
    ///             <OTN>NodeValue</OTN>
    ///         </ON>
    ///         <OTN>NodeValue</OTN>
    ///         <ON>
    ///             <OCN><!--Comment--></OCN>
    ///             <OTN>NodeValue</OTN>
    ///         </ON>
    ///     </ON>
    /// </ON>
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// </summary>
    public class OutputNode : IOutputNode, IDisposable
    {
        XmlQualifiedName Name;
        String prefix;
        bool StartNodeNotClosed;
        MemoryStream StartElement;
        protected MemoryStream ElementValue;
        MemoryStream EndElement;
        public List<OutputNode> Children;
        List<String> ProcessingInstructions;
        protected NodeProcessor nodeProcessor;

        protected OutputSettings oSettings;
        XmlSchemaSet oSchemaSet;
        XPathNavigator oXPathNav;
        protected Encoding oOutputEncoding;                         // The output format of the XML (e.g. Unicode, UTF-8, UTF-7 etc)
        protected OutputNode oParent;
        protected bool bHasSimpleContent;       // Used to determine if this node should encode itself for output, it it is a simple type
                                                // then it's child should encode itself, and not the parent.

        public OutputNode(OutputSettings Settings, OutputNode Parent, List<String> PIs, XPathNavigator XPathNav)
        {
            // Copy
            oSettings = Settings;
            oSchemaSet = oSettings.XMLDoc.Schemas;
            oOutputEncoding = oSettings.XmlSettings.Encoding;
            oParent = Parent;
            if (PIs != null)
                ProcessingInstructions = PIs.GetRange(0, PIs.Count);
            else
                ProcessingInstructions = new List<string>();
            oXPathNav = XPathNav;

            // Initialise
            StartElement = new MemoryStream();
            ElementValue = new MemoryStream();
            EndElement = new MemoryStream();
            Children = new List<OutputNode>();
            
        }

        #region IDisposable Members

        /// <summary>
        /// Call this to free up all the resources of he object
        /// </summary>
        public void Dispose()
        {
            if (null != StartElement)
            {
                StartElement.Dispose();
                StartElement = null;
            }
            if (null != ElementValue)
            {
                ElementValue.Dispose();
                ElementValue = null;
            }
            if (null != ElementValue)
            {
                EndElement.Dispose();
                EndElement = null;
            }
            for (int i = 0; i < Children.Count; i++)
                Children[i].Dispose();
            Children.Clear();
            Children = null;

            ProcessingInstructions.Clear();
            ProcessingInstructions = null;
            if (null != nodeProcessor)
                nodeProcessor.Dispose();
            nodeProcessor = null;
            oSettings = null;
            oSchemaSet = null;
            oXPathNav = null;
            oOutputEncoding = null;
            oParent = null;

            GC.SuppressFinalize(this);
        }

        #endregion

        protected void WriteToStream(MemoryStream stream, byte[] bytes)
        {
            stream.Write(bytes, 0, bytes.Length);
        }

        protected void WriteToStream(MemoryStream stream, String str)
        {
            byte[] bytes = oOutputEncoding.GetBytes(str);
            WriteToStream(stream, bytes);
        }

        protected void WriteIndentToStream(MemoryStream stream, int IndentSize)
        {
            if(oSettings.XmlSettings.Indent)
                for (int i = 0; i < IndentSize; i++)
                    WriteToStream(stream, oSettings.XmlSettings.IndentChars);
        }

        protected void WriteLineBreakStream(MemoryStream stream)
        {
            if (oSettings.XmlSettings.Indent)
                WriteToStream(stream, oSettings.XmlSettings.NewLineChars);
        }

        public void WriteStartElement(String prefix, String localName, String ns)
        {
            // Remember the node info
            this.Name = new XmlQualifiedName(localName, ns);
            this.prefix = prefix;

            // Set the node processor for this node
            nodeProcessor = new NodeProcessor(Name, oSettings, this);
            nodeProcessor.AddXMLProcessingInstructions(ProcessingInstructions);

            // Write the start element
            String StartElementStr = "<";
            // Write out prefix
            if (!String.IsNullOrEmpty(prefix))
                StartElementStr += (prefix + ":");
            // Write out name
            StartElementStr += localName;
            WriteToStream(StartElement, oOutputEncoding.GetBytes(StartElementStr));
            StartNodeNotClosed = true;
        }

        /// <summary>
        /// Used to reset the Node element value.  Usually called when a Node may have already been serialised, but due to an update,
        /// needs to be re-serialised.
        /// </summary>
        public void ResetElementValue()
        {
            if (0 != ElementValue.Length)
                ElementValue.SetLength(0);
        }

        /// <summary>
        /// Reset all the element Nodes up until the specified ancestor of this Node.
        /// </summary>
        /// <param name="Ancestor"></param>
        public void ResetElementValueToAncestor(OutputNode Ancestor)
        {
            OutputNode Child = this;
            while (null != Child.Parent)
            {
                Child.Parent.ResetElementValue();

                if (0 == String.Compare(Child.Parent.Name.ToString(), Ancestor.Name.ToString()))
                    return;

                Child = Child.Parent;
            }
        }

        /// <summary>
        /// Reset all the element Nodes up until the specified ancestor of this Node.
        /// </summary>
        /// <param name="Ancestor"></param>
        public void ResetElementValueToRoot()
        {
            OutputNode Child = this;
            while (null != Child.Parent)
            {
                Child.Parent.ResetElementValue();

                Child = Child.Parent;
            }
        }

        /// <summary>
        /// Reset and update the Node element value.
        /// </summary>
        /// <param name="Value"></param>
        public void ResetWriteElementValue(String Value)
        {
            ResetElementValue();
            Children.Clear();
            OutputTextNode tn = new OutputTextNode(oSettings, this, null, oXPathNav.Clone());
            tn.WriteElementValue(Value);
            Children.Add(tn as OutputNode);
        }

        public virtual void WriteElementValue(String Value)
        {
            OutputTextNode tn = new OutputTextNode(oSettings, this, null, oXPathNav.Clone());
            tn.WriteElementValue(Value);
            Children.Add(tn as OutputNode);
        }

        public void WriteProcInst(string text)
        {
            OutputPINode pin = new OutputPINode(oSettings, this, null, oXPathNav.Clone());
            pin.WriteElementValue(text);
            Children.Add(pin as OutputNode);
        }

        public void WriteComment(string text)
        {
            OutputCommentNode cn = new OutputCommentNode(oSettings, this, null, oXPathNav.Clone());
            cn.WriteElementValue(text);
            Children.Add(cn as OutputNode);
        }

        public void WriteCData(string text)
        {
            OutputTextNode cn = new OutputCDATANode(oSettings, this, null, oXPathNav.Clone());
            cn.WriteElementValue(text);
            Children.Add(cn as OutputNode);
        }

        public void WriteFullEndElement()
        {
            String EndElementStr = "</";
            if (!String.IsNullOrEmpty(prefix))
                EndElementStr += (prefix + ":");
            EndElementStr += Name.Name;
            EndElementStr += ">";
            WriteToStream(EndElement, oOutputEncoding.GetBytes(EndElementStr));
        }

        public void WriteEndElement()
        {
            WriteToStream(StartElement, oOutputEncoding.GetBytes(" />"));
            StartNodeNotClosed = false;
        }

        public void WriteStartAttribute(string prefix, string localName, string ns)
        {            
            String StartAtt = " ";
            if (!String.IsNullOrEmpty(prefix))
                StartAtt += (prefix + ":");
            StartAtt += localName;
            StartAtt += "=\"";
            WriteToStream(StartElement, oOutputEncoding.GetBytes(StartAtt));
        }

        public void WriteAttributeValue(String Value)
        {
            WriteToStream(StartElement, oOutputEncoding.GetBytes(Value));
        }

        public void WriteEndAttribute()
        {
            WriteToStream(StartElement, oOutputEncoding.GetBytes("\""));
        }

        public void AddChild(OutputNode Child)
        {
            Children.Add(Child);
        }

        /// <summary>
        /// Use this version of Serialise to serialise the root node and release all memory as we serialise.  The
        /// OutputNode will become unusable after this is called.
        /// </summary>
        /// <param name="OutputStream"></param>
        public void SerialiseAndClose(MemoryStream OutputStream)
        {
            Serialise(OutputStream, false, 0, true, true);
        }

        /// <summary>
        /// Use this version of Serialise to serialise a particular node, or if you are not doing a final serialise
        /// </summary>
        /// <param name="OutputStream"></param>
        /// <param name="SuppressComments"></param>
        /// <param name="IndentSize"></param>
        public void Serialise(MemoryStream OutputStream, bool SuppressComments, int IndentSize, bool AllowDontOutput)
        {
            Serialise(OutputStream, SuppressComments, IndentSize, false, AllowDontOutput);
        }

        /// <summary>
        /// Serialise a node to output.  The ability to suppress comments is necessary as when we process a node (due to some Processing 
        /// Instruction) we would like the processing to occur on the node as if it had no comments.  The SuppressComments parameter is not
        /// used to decide, from a formatting point of view, if comments are output, there is a global setting and it obeys the specified
        /// markup setting of its parent.
        /// </summary>
        /// <param name="OutputStream"></param>
        /// <param name="SuppressComments"></param>
        protected virtual void Serialise(MemoryStream OutputStream, bool SuppressComments, int IndentSize, bool ActiveCleanup, bool AllowDontOutput)
        {
            if (AllowDontOutput && nodeProcessor.bDontOutput)
                return;

            // Serialise the start element
            if (nodeProcessor.ShouldWriteMarkup())
            {
                if (StartNodeNotClosed)
                {
                    WriteToStream(StartElement, oOutputEncoding.GetBytes(">"));
                    StartNodeNotClosed = false;
                }
                StartElement.Flush();
                WriteIndentToStream(OutputStream, IndentSize);
                StartElement.WriteTo(OutputStream);
                if(!bHasSimpleContent)
                    WriteLineBreakStream(OutputStream);
            }
            if (ActiveCleanup)
            {
                StartElement.Dispose();
                StartElement = null;
            }

            // Since ElementValue is only written to here, if it has a value then this node's value has already been serialised and
            // so we don't do that twice.
            if ((0 != Children.Count) && (0 == ElementValue.Length))
            {
                // Serialise children
                for (int i = 0; i < Children.Count; i++)
                {
                    Children[i].Serialise(ElementValue, SuppressComments, IndentSize + 1, ActiveCleanup, AllowDontOutput);
                }
            }
            
            // Element value are written to stream as raw bytes, so stream contains raw bytes (of the specified output encoding)
            
            if (!bHasSimpleContent)
            {
                // Encode the value according to Schema attribute instructions
                byte[] nodevalue = EncodingHelper.EncodeForOutput(ElementValue.ToArray(), nodeProcessor.TypeCode, nodeProcessor.outputType, System.Text.UnicodeEncoding.Unicode, oSettings.XmlSettings.Encoding);
                OutputStream.Write(nodevalue, 0, nodevalue.Length);
            }
            else
            {
                // The node has already been encoded properly
                ElementValue.Flush();
                ElementValue.WriteTo(OutputStream);
            }
            if (ActiveCleanup)
            {
                ElementValue.Dispose();
                ElementValue = null;
                for (int i = 0; i < Children.Count; i++)
                {
                    Children[i].Dispose();
                }
                Children.Clear();
            }

            // Serialise the end element
            if (nodeProcessor.ShouldWriteMarkup())
            {
                if (!bHasSimpleContent)
                    WriteIndentToStream(OutputStream, IndentSize);
                EndElement.Flush();
                EndElement.WriteTo(OutputStream);
                WriteLineBreakStream(OutputStream);
            }
            if (ActiveCleanup)
            {
                EndElement.Dispose();
                EndElement = null;
            }
        }

        public bool HasAncestor(OutputNode Ancestor)
        {
            OutputNode Child = this;
            while (null != Child.Parent)
            {
                if (0 == String.Compare(Child.Parent.Name.ToString(), Ancestor.Name.ToString()))
                    return true;

                Child = Child.Parent;
            }
            return false;
        }

        #region Properties
        public NodeProcessor Processor
        {
            get
            {
                return nodeProcessor;
            }
        }

        public OutputNode Parent
        {
            get
            {
                return oParent;
            }
        }

        public bool TargetNode
        {
            get
            {
                return nodeProcessor.TargetNode;
            }
        }

        public bool HasSimpleContent
        {
            set
            {
                bHasSimpleContent = value;
            }
        }
        #endregion

        #region IOutputNode Members

        public XmlQualifiedName QName
        {
            get { return Name; }
        }

        public XPathNavigator XPathNav
        {
            get { return oXPathNav.Clone(); }
        }

        public Encoding OutputEncoding
        {
            get { return oOutputEncoding; }
        }

        public String PIText
        {
            get { return nodeProcessor.PItext; }
        }

        public TypeRestrictor Restrictor
        {
            get 
            {
                XMLElementIdentifier Id = new XMLElementIdentifier(Name, oXPathNav.SchemaInfo.SchemaElement, oSchemaSet);
                return (oSettings.ObjectDB[Id] as ElementDBEntry).FacetRestrictor; 
            }
        }

        public Coder.OutputAsType OutputAs
        {
            get { return nodeProcessor.outputType; }
        }

        public DataSchemaTypeCode TypeCode
        {
            get { return nodeProcessor.TypeCode; }
        }

        #endregion
    }

    class OutputTextNode : OutputNode
    {
        // It is important to keep track of the string value right up to the point of serialisation, otherwise we might convert between
        // two string encodings and lose information
        protected String ElementStrValue;

        public OutputTextNode(OutputSettings Settings, OutputNode Parent, List<String> PIs, XPathNavigator XPathNav)
            : base(Settings, Parent, PIs, XPathNav)
        {
            oParent = Parent;
            Parent.HasSimpleContent = true;
        }

        public override void WriteElementValue(String Value)
        {
            ElementStrValue = Value;
        }

        protected override void Serialise(MemoryStream OutputStream, bool SuppressComments, int IndentSize, bool ActiveCleanup, bool AllowDontOutput)
        {
            byte[] nodevalue = EncodingHelper.EncodeStringForOutput(ElementStrValue, oParent.TypeCode, oParent.OutputAs, System.Text.UnicodeEncoding.Unicode, oOutputEncoding);
            OutputStream.Write(nodevalue, 0, nodevalue.Length);
            nodevalue = null;
            if (ActiveCleanup)
                ElementStrValue = null;
        }
    }

    class OutputCommentNode : OutputTextNode
    {
        byte[] StartComment;
        byte[] EndComment;

        public OutputCommentNode(OutputSettings Settings, OutputNode Parent, List<String> PIs, XPathNavigator XPathNav)
            : base(Settings, Parent, PIs, XPathNav)
        {
            Parent.HasSimpleContent = false;
            StartComment = Settings.XmlSettings.Encoding.GetBytes("<!--");
            EndComment = Settings.XmlSettings.Encoding.GetBytes("-->");
        }

        protected override void Serialise(MemoryStream OutputStream, bool SuppressComments, int IndentSize, bool ActiveCleanup, bool AllowDontOutput)
        {
            if (!SuppressComments && Parent.Processor.ShouldWriteMarkup() && !oSettings.Config.OmitComments)
            {
                WriteIndentToStream(OutputStream, IndentSize);
                OutputStream.Write(StartComment, 0, StartComment.Length);
                OutputStream.Write(oOutputEncoding.GetBytes(ElementStrValue), 0, oOutputEncoding.GetByteCount(ElementStrValue));
                OutputStream.Write(EndComment, 0, EndComment.Length);
                WriteLineBreakStream(OutputStream);
                if (ActiveCleanup)
                {
                    StartComment = null;
                    EndComment = null;
                    ElementStrValue = null;
                }
            }
        }
    }

    class OutputPINode : OutputTextNode
    {
        byte[] StartPI;
        byte[] EndPI;

        public OutputPINode(OutputSettings Settings, OutputNode Parent, List<String> PIs, XPathNavigator XPathNav)
            : base(Settings, Parent, PIs, XPathNav)
        {
            Parent.HasSimpleContent = false;
            StartPI = Settings.XmlSettings.Encoding.GetBytes("<?");
            EndPI = Settings.XmlSettings.Encoding.GetBytes("?>");
        }

        protected override void Serialise(MemoryStream OutputStream, bool SuppressComments, int IndentSize, bool ActiveCleanup, bool AllowDontOutput)
        {
            if (!SuppressComments && Parent.Processor.ShouldWriteMarkup())
            {
                WriteIndentToStream(OutputStream, IndentSize);
                OutputStream.Write(StartPI, 0, StartPI.Length);
                OutputStream.Write(oOutputEncoding.GetBytes(ElementStrValue), 0, oOutputEncoding.GetByteCount(ElementStrValue));
                OutputStream.Write(EndPI, 0, EndPI.Length);
                WriteLineBreakStream(OutputStream);
                if (ActiveCleanup)
                {
                    StartPI = null;
                    EndPI = null;
                    ElementStrValue = null;
                }
            }
        }
    }

    class OutputCDATANode : OutputTextNode
    {
        byte[] StartCDATA;
        byte[] EndCDATA;

        public OutputCDATANode(OutputSettings Settings, OutputNode Parent, List<String> PIs, XPathNavigator XPathNav)
            : base(Settings, Parent, PIs, XPathNav)
        {
            StartCDATA = Settings.XmlSettings.Encoding.GetBytes("<![CDATA[");
            EndCDATA = Settings.XmlSettings.Encoding.GetBytes("]]>");
        }

        protected override void Serialise(MemoryStream OutputStream, bool SuppressComments, int IndentSize, bool ActiveCleanup, bool AllowDontOutput)
        {
            if(Parent.Processor.ShouldWriteMarkup())
                OutputStream.Write(StartCDATA, 0, StartCDATA.Length);
            base.Serialise(OutputStream, SuppressComments, IndentSize, ActiveCleanup, AllowDontOutput);
            if (Parent.Processor.ShouldWriteMarkup())
                OutputStream.Write(EndCDATA, 0, EndCDATA.Length);
            if (ActiveCleanup)
            {
                StartCDATA = null;
                EndCDATA = null;
                ElementStrValue = null;
            }
        }
    }
}
